/**
 * Copyright (C) 2012 The SkyTvOS Project
 * <p/>
 * Version     Date           Author
 * ─────────────────────────────────────
 * 2014年11月3日         Root.Lu
 */

package com.coocaa.x.service.litedownloader.downloader;

import com.coocaa.x.framework.data.JObject;
import com.coocaa.x.framework.utils.Android;
import com.coocaa.x.framework.utils.CacheTool;
import com.coocaa.x.framework.utils.LogUtils;
import com.coocaa.x.framework.utils.Storage;
import com.coocaa.x.framework.utils.Storage.SDevice;
import com.coocaa.x.framework.utils.Storage.SDevice.MEDIA_TYPE;
import com.coocaa.x.service.litedownloader.data.DownloadError;
import com.coocaa.x.service.litedownloader.data.DownloadTask;
import com.coocaa.x.service.litedownloader.data.ProcessInfo;
import com.coocaa.x.service.litedownloader.data.TaskInfo;
import com.coocaa.x.service.litedownloader.data.http.HttpDownloaderInfo;
import com.coocaa.x.service.litedownloader.db.DownloaderDB;
import com.coocaa.x.service.litedownloader.db.DownloaderTable;
import com.coocaa.x.service.litedownloader.downloader.http.HttpDownloader;
import com.coocaa.x.service.litedownloader.downloader.http.HttpDownloaderTable;

import java.io.File;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;

public abstract class Downloader implements Runnable {
    public static Downloader create(DownloadTask task) {
        Downloader ret = new HttpDownloader();
        ret.setDownloadTask(task);
        ret.createtime = task.createtime;
        return ret;
    }

    public interface DownloaderListener {
        void onReady(DownloadTask task);

        void onPrepare(DownloadTask task);

        void onStart(DownloadTask task);

        void onStop(DownloadTask task);

        void onFinish(DownloadTask task);

        void onDelete(DownloadTask task);

        void onProcessInfo(DownloadTask task, ProcessInfo info);

        void onError(DownloadTask task, DownloadError error);
    }

    public class OnErrorException extends Exception {
        /**
         * @Fields serialVersionUID TODO(write something)
         */
        private static final long serialVersionUID = -1242081093285115605L;
        public DownloadError error = null;

        public OnErrorException(DownloadError error) {
            super();
            this.error = error;
        }
    }

    public class OnStopException extends Exception {
        /**
         * @Fields serialVersionUID TODO(write something)
         */
        private static final long serialVersionUID = 5539965820917633489L;
    }

    public class OnDeleteException extends Exception {
        /**
         * @Fields serialVersionUID TODO(write something)
         */
        private static final long serialVersionUID = -1014700838147515744L;
    }

    private enum DOWNLOADER_STATE {
        NOT_RUNNING,
        RUNNING_NOT_PREPARED,
        PREPARED_NOT_START,
        PROCESSING,
        DOING_STOP,
        DOING_DELETE,
    }

    private static final float TIMER_INTERVAL = 2.0f;
    private Timer timer = null;
    private DownloaderListener listener = null;
    private DownloadTask task = null;
    public Thread thread = null;
    public long createtime = 0, starttime = 0;
    private long length = 0;

    private DOWNLOADER_STATE state = DOWNLOADER_STATE.NOT_RUNNING;

    public final void setDownloadTask(DownloadTask task) {
        this.task = task;
    }

    public final DownloadTask getDownloadTask() {
        return task;
    }

    public final void setListener(DownloaderListener listener) {
        this.listener = listener;
    }

    private static class MD5Error extends JObject {
        public String result = "failed";
        public String url;
        public String error_md5, expect_md5;
    }

    @Override
    public final void run() {
        // TODO Auto-generated method stub
        try {
            synchronized (this) {
                if (checkStoppedOrDeletedBeforeRunning())
                    return;
                state = DOWNLOADER_STATE.RUNNING_NOT_PREPARED;
            }
            thread = Thread.currentThread();
            starttime = System.currentTimeMillis();
            task.info = _prepare();
            LogUtils.d("downloader run onprepare:" + task.info);
            if (listener != null)
                listener.onPrepare(task);
            length = task.info.length;
            synchronized (this) {
                checkAlreadyStopOrDelete();
                state = DOWNLOADER_STATE.PREPARED_NOT_START;
            }
            _start();
            if (listener != null)
                listener.onStart(task);
            synchronized (this) {
                checkAlreadyStopOrDelete();
                state = DOWNLOADER_STATE.PROCESSING;
            }
            if (timer != null) {
                timer.cancel();
                timer = null;
            }
            timer = new Timer();
            timer.schedule(new TimerTask() {
                float prev_current = -1, current = -1;

                @Override
                public void run() {
                    if (state == DOWNLOADER_STATE.PROCESSING) {
                        current = _current();
                        if (prev_current == -1)
                            prev_current = current;
                        else {
                            float speed = (current - prev_current) / TIMER_INTERVAL;
                            prev_current = current;

                            if (listener != null) {
                                ProcessInfo info = new ProcessInfo();
                                info.current = (long) current;
                                info.length = length;
                                info.speed = speed;
                                info.percent = (int) (current * 100 / length);
                                listener.onProcessInfo(task, info);
                            }
                        }
                    }
                }
            }, 0, (int) TIMER_INTERVAL * 1000);
            _process();
            try {
                checkMD5();
                if (listener != null)
                    listener.onFinish(task);
            } catch (OnErrorException e1) {
                synchronized (this) {
                    state = DOWNLOADER_STATE.NOT_RUNNING;
                }
                listener.onError(task, e1.error);
            }
        } catch (OnStopException e) {
            LogUtils.d("*********************************OnStopException!! listener:" + listener);
            if (listener != null)
                listener.onStop(task);
        } catch (OnDeleteException e) {
            if (listener != null)
                listener.onDelete(task);
        } catch (OnErrorException e) {
            if (listener != null) {
                listener.onError(task, e.error);
                listener.onStop(task);
            }
        } finally {
            if (timer != null)
                timer.cancel();
            _release();
            timer = null;
            thread = null;
            starttime = 0;
        }

        synchronized (this) {
            state = DOWNLOADER_STATE.NOT_RUNNING;
        }
    }

    private final void checkMD5() throws OnErrorException {
        if (getDownloadTask().md5.equals(DownloadTask.WITH_OUT_MD5))
            return;
        LogUtils.SubmitLog log = new LogUtils.SubmitLog();
        log.id = "download_md5_oncheck";
        log.productID = LogUtils.PRODUCTID_APPSTORE;
        String md5 = Android.getFileMD5(new File(_getSaveFilePath())).toLowerCase();
        if (!md5.equals(getDownloadTask().md5.toLowerCase())) {
            DownloadError e = new DownloadError();
            e.errcode = DownloadError.ERRCODE_MD5_CHECK_ERROR;
            e.error = "expect md5:" + getDownloadTask().md5 + " but " + md5;

            MD5Error error = new MD5Error();
            error.result = "failed";
            error.url = getDownloadTask().url;
            error.expect_md5 = getDownloadTask().md5;
            error.error_md5 = md5;
            log.addParam("result", error.result);
            log.addParam("url", error.url);
            log.addParam("md5", "expect:" + error.expect_md5 + " error:" + error.error_md5);
            log.addParam("info", error.toJSONString());
            LogUtils.submit(log);
            throw new OnErrorException(e);
        }
        log.addParam("result", "success");
        LogUtils.submit(log);
    }

    public final void ready() {
        if (listener != null)
            listener.onReady(task);
    }

    public final int stop() {
        int ret = DownloadTask.TASK_CODE_OK;
        boolean flag = false;
        synchronized (this) {
            LogUtils.d("downloader stop state:" + state);
            if (state == DOWNLOADER_STATE.NOT_RUNNING) {
                state = DOWNLOADER_STATE.DOING_STOP;
                _release();
                if (listener != null)
                    listener.onStop(task);
            } else if (state != DOWNLOADER_STATE.DOING_STOP) {
                flag = true;
                state = DOWNLOADER_STATE.DOING_STOP;
            } else {
                LogUtils.d("doing stop no need to stop again!!");
                ret = DownloadTask.TASK_CODE_ALREADY_DOING_STOP;
            }
        }
        if (flag)
            _stop();
        return ret;
    }

    public final int delete() {
        int ret = DownloadTask.TASK_CODE_OK;
        boolean flag = false;
        synchronized (this) {
            if (state == DOWNLOADER_STATE.NOT_RUNNING) {
                state = DOWNLOADER_STATE.DOING_DELETE;
                _do_delete();
                if (listener != null)
                    listener.onDelete(task);
            } else if (state != DOWNLOADER_STATE.DOING_DELETE) {
                flag = true;
                state = DOWNLOADER_STATE.DOING_DELETE;
            } else {
                LogUtils.d("doing delete no need to delete again!!");
                ret = DownloadTask.TASK_CODE_ALREADY_DOING_DELETE;
            }
        }
        if (flag)
            _delete();
        return ret;
    }

    private boolean checkStoppedOrDeletedBeforeRunning() {
        boolean ret = false;
        synchronized (this) {
            ret = (state == DOWNLOADER_STATE.DOING_STOP)
                    || (state == DOWNLOADER_STATE.DOING_DELETE);

            state = DOWNLOADER_STATE.NOT_RUNNING;
        }
        return ret;
    }

    private void checkAlreadyStopOrDelete() throws OnStopException, OnDeleteException {
        synchronized (this) {
            if (state == DOWNLOADER_STATE.DOING_STOP)
                throw new OnStopException();
            else if (state == DOWNLOADER_STATE.DOING_DELETE)
                throw new OnDeleteException();
        }
    }

    //    protected static Map<String, Long> downloadedSpaceMap = new HashMap<String, Long>();
    protected static Map<String, Long> getDownloadingNeedSpace() {
        Map<String, Long> downloadedSpaceMap = new HashMap<String, Long>();
        long needSpace = 0;
        DownloaderTable dtable = null;
        CacheTool<String, DownloadTask> taskcache = null;
        HttpDownloaderTable table = null;
        dtable = (DownloaderTable) DownloaderDB.getInstance().getTable(DownloaderTable.class);
        taskcache = new CacheTool<String, DownloadTask>(dtable);
        taskcache.flush();

        try {
            table = (HttpDownloaderTable) DownloaderDB.getInstance().getTable(
                    HttpDownloaderTable.class);
        } catch (Exception e) {
            e.printStackTrace();
        }

        if (taskcache == null || table == null)
            return null;

        List<DownloadTask> list = taskcache.values();

        for (DownloadTask task : list) {
            LogUtils.i("1029", "state: " + task.info.state + "path: " + task.savepath);

            if (task.savepath != null && !task.savepath.equals("")) {

                HttpDownloaderInfo downloaderInfo = table.get(task.ID);
                if (downloaderInfo != null) {
                    if (downloadedSpaceMap.containsKey(task.savepath)) {
                        needSpace = downloadedSpaceMap.get(task.savepath);
                        needSpace += downloaderInfo.length - downloaderInfo.current;
                        LogUtils.i("1029", "needSpace: " + needSpace);
                        downloadedSpaceMap.put(task.savepath, needSpace);
                    } else {
                        needSpace += downloaderInfo.length - downloaderInfo.current;
                        downloadedSpaceMap.put(task.savepath, needSpace);
                    }
                } else {
                    continue;
                }
            }
        }

        return downloadedSpaceMap;
    }

    protected static synchronized String getSavePath(DownloadTask task, long fileLength) {
        List<SDevice> list = Storage.getSDeviceList();
        SDevice maxFreeDevice = null;
        long totalSpace;
        long configSize = Storage.getConfigSize();
        long need;
        String path;

        Storage.ThreadSleep(); //尽可能等上一个task计算完需要的空间之后再去获取路径，减少下载过程中空间不足出现的几率
        LogUtils.i("xfk", "start getDownloadingNeedSpace");
        Map<String, Long> downloadedSpaceMap = getDownloadingNeedSpace();
        LogUtils.i("xfk", "end getDownloadingNeedSpace = " + downloadedSpaceMap.size());
        for (SDevice device : list) {
            if (device.getMediaType() == MEDIA_TYPE.SDCARD) {
                totalSpace = Storage.getTotalSpace(device, false);
                path = Storage.getSDCardPath("coocaa_app/download/");
                if (downloadedSpaceMap.containsKey(path))
                    need = downloadedSpaceMap.get(path);
                else
                    need = 0;
                need += fileLength;
                LogUtils.i("xfk", "need: " + need + ", path: " + path);
                if (Storage.checkNewTaskCanDownLoad(path, need, configSize, totalSpace)) {
                    return path;
                }

                continue;
            } else {
                if (maxFreeDevice == null)
                    maxFreeDevice = device;
                else {
                    if (Storage.getFreeSpace(maxFreeDevice, false) < Storage.getFreeSpace(device,
                            false))
                        maxFreeDevice = device;
                }
            }
        }

        path = Storage.getInternalMemoryPath("coocaa_app/download/");
        totalSpace = Storage.getTotalSpace(path, false);
        if (downloadedSpaceMap.containsKey(path))
            need = downloadedSpaceMap.get(path);
        else
            need = 0;
        need += fileLength;
        LogUtils.i("xfk", "need: " + need + ", path: " + path);
        if (Storage.checkNewTaskCanDownLoad(path, need, configSize, totalSpace)) {
            return path;
        }

        if (maxFreeDevice != null) {
            totalSpace = Storage.getTotalSpace(maxFreeDevice, false);
            path = Storage.getSDevicePath(maxFreeDevice, "coocaa_app/download/");
            if (downloadedSpaceMap.containsKey(path))
                need = downloadedSpaceMap.get(path);
            else
                need = 0;
            need += fileLength;
            LogUtils.i("1029", "need: " + need + ", path: " + path);
            if (Storage.checkNewTaskCanDownLoad(path, need, configSize, totalSpace)) {
                return path;
            }
        }

        return null;
    }

    // protected static synchronized boolean hasSufficientSpace(String path, long fileLength)
    // {
    // long memorySpace = getMemorySpace(path);
    // LogUtils.d("path:" + path + "  memorySpace:" + memorySpace + "   fileLength:" + fileLength);
    // if (memorySpace < fileLength + 100 * 1024 * 1024)
    // return false;
    // return true;
    // }
    //
    // protected static synchronized long getMemorySpace(String path)
    // {
    // return SkySystemUtil.getAvailSpace(path);
    // }

    protected abstract TaskInfo _prepare() throws OnErrorException;

    protected abstract void _start();

    protected abstract void _stop();

    protected abstract void _delete();

    protected abstract void _do_delete();

    protected abstract void _process() throws OnStopException, OnDeleteException, OnErrorException;

    protected abstract float _current();

    protected abstract void _release();

    protected abstract String _getSaveFilePath();

}
